跳到主要内容

Go 套接字 Socket 编程 - UDP 数据报

在一个无连接的协议(UDP)中,每个消息都包含了关于它的来源和目的地的信息。UDP 客户端和服务器使用的数据包,单独包含来源和目的地的信息。(它没有 Session)

UDP 数据报

Go 下处理 TCP 和 UDP 之间的主要区别是如何处理多个客户端可能同时有数据包到达,没有一个管理 TCP Session 的缓冲。

它主要需要调用的几个函数:

func ResolveUDPAddr(network, address string) (*UDPAddr, error)
func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error)
func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error)
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)

UDP 客户端编写

UDP 时间服务的客户端并不需要做很多的变化,仅仅改变 TCP 调用为 UDP 调用:

提示

这里 client 的写是 Write,读是 Read。要与下面服务端的 ReadFrom 和 WriteTo 做个区分

func main() {
udpAddr, err := net.ResolveUDPAddr("udp", ":1200")
checkError(err)

conn, err := net.DialUDP("udp", nil, udpAddr)
checkError(err)

_, err = conn.Write([]byte("hello world"))
checkError(err)

var buf [512]byte
n, err := conn.Read(buf[0:])
checkError(err)

fmt.Println(string(buf[:n]))
}

func checkError(err error) {
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
}

编写服务端

服务端也很简单,从每个 UDP 包里面取得地址,再把数据回传回去,读是通过 listener.ReadFromUDP,写通过 listener.WriteToUDP

func main() {
service := ":1200"
udpAddr, err := net.ResolveUDPAddr("udp", service)
checkError(err)

conn, err := net.ListenUDP("udp", udpAddr)
checkError(err)

for {
handleClient(conn)
}
}

func handleClient(conn *net.UDPConn) {
var buf [512]byte
// 读取了地址再返回回去
_, addr, err := conn.ReadFromUDP(buf[0:])
if err != nil {
return
}

daytime := time.Now().String()
// 返回时间给客户端
conn.WriteToUDP([]byte(daytime), addr)
}

func checkError(err error) {
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
}

调用通用 API

上面是通过调用 DialUDP 来建立连接的,实际上可以使用通用 API 来完成

迄今为止我们已经区分 TCP 和 UDP API 的不同,使用例子 DialTCP 和 DialUDP 分别返回一个 TCPConn 和 UDPConn。实际上这两者可以使用一个简单的函数,而不是单独使用 TCP 和 UDP 的 dial 函数。

// 函数原型
func Dial(network, address string) (Conn, error) {
var d Dialer
return d.Dial(network, address)
}

它返回的 Conn 类型是一个接口,TCPConn 和 UDPConn 实现了该接口。在很大程度上,你可以通过该接口处理而不是用这两种类型。

只需要简单的更改一下上面的代码就行了。

func main() {
conn, err := net.Dial("udp", ":1200")
checkError(err)

_, err = conn.Write([]byte("hello world"))
checkError(err)

var buf [512]byte
n, err := conn.Read(buf[0:])
checkError(err)

fmt.Println(string(buf[:n]))
}
// ...

使用 ListenPacket 函数同样可以简化一个服务器的编写,不过它使用的是 PacketConn 而不是 Conn,所以返回的是 PacketConn

func ListenPacket(net, laddr string) (c PacketConn, err os.Error)

PacketConn 这个接口的主要方法 ReadFrom 和 WriteTo 用来处理数据包的读取和写入。

Read 和 Write 方法集的比较

  • 如果 *UDPConn 是 connected,读写方法是 Read 和 Write。如上面的 Client
  • 如果 *UDPConn 是 unconnected,读写方法是 ReadFromUDP 和 WriteToUDP(以及 ReadFrom 和WriteTo)。如上面的 Server
备注

UDP 服务器端在调用 net.ListenUDP() 后创建 net.UDPConn,read/write 操作是通过这个 UDPConn 来完成的。因为 listen 的时候只指定了本地绑定的地址,它只能被动的接收来自客户端的消息,因此这个 UDPConn 在 Go 中为 unconnected 类型。

这种类型的 UDPConn 的读操作可以接受 Read()ReadFromUDP()。区别是 Read() 无法知道远程连接的地址信息而 ReadFromUDP() 可以,所以如果后续需要跟远程进行双向通讯需要使用 ReadFromUDP()

还有几种情况需要弄清楚:

1、因为 unconnected 的 *UDPConn 还没有目标地址,所以需要把目标地址当作参数传入到 WriteToUDP 的方法中,但是 unconnected 的 *UDPConn 可以调用 Read 方法吗?

答案是可以,但是在这种情况下,客户端的地址信息就被忽略了。

func main() {
listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9981})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Local: <%s> \n", listener.LocalAddr().String())
data := make([]byte, 1024)
for {
n, err := listener.Read(data)
if err != nil {
fmt.Printf("error during read: %s", err)
}
fmt.Printf("<%s>\n", data[:n])
}
}

2、unconnected 的 *UDPConn 可以调用 Write 方法吗? 答案是不可以, 因为不知道目标地址。

3、connected 的 *UDPConn 可以调用 WriteToUDP 方法吗? 答案是不可以, 因为目标地址已经设置。 即使是相同的目标地址也不可以。

func main() {
ip := net.ParseIP("127.0.0.1")
srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 0}
dstAddr := &net.UDPAddr{IP: ip, Port: 9981}
conn, err := net.DialUDP("udp", srcAddr, dstAddr)
if err != nil {
fmt.Println(err)
}
defer conn.Close()
_, err = conn.WriteToUDP([]byte("hello"), dstAddr)
if err != nil {
fmt.Println(err)
}
}

报错:

write udp 127.0.0.1:50141->127.0.0.1:9981: use of WriteTo with pre-connected connection

References

《Go网络编程》 深入Go UDP编程